/* JAT: Java Astrodynamics Toolkit
 * 
  Copyright 2012 Tobias Berthold

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */

// Original Code under LGPL
// http://java.freehep.org/freehep-java3d/license.html


package jat.jat3D.plot3D;



public class AxisLabelCalculator
{

    /* VARIABLES */
    private double data_min = 0d, data_max = 1d; // actual min/max for the data set
    private double plot_min = 0d, plot_max = 1d; // min/max on the axis itself
    private AxisLabel[] labels;
    private int nDivisions = 0;
    private boolean labelsValid;

    public void createNewLabels(double min, double max)
    {
    	data_min = min;
    	data_max = max;
    	int minNumberOfDivisions = 1;
    	int maxNumberOfDivisions = 10;
    	double log10 = Math.log(10.0);
    	int maxCharsPerLabel = 5;
        labelsValid = true;

        // log_max is the logarithm of the max value of the data
        double log_max = data_max == 0d ? 0d : Math.log(Math.abs(data_max)) / log10;

        // int_log_max is the floored integer equivalent of log_max
        final int int_log_max = (int) Math.floor(log_max);

        // by default, scale_power is 0
        // This number represents the amount by which we scale all labels.  For example,
        // if our range is from 2,000,000,000 to 5,000,000,000 we want scale_power to be
        // 9 so that we get 2.0, 2.5, 3.0, ... or something like that on the labels.
        int scale_power = 0;
        if (int_log_max >= maxCharsPerLabel)
            // we have an order of magnitude that can't be displayed in standard form, so we need to set
            // a value for scale_power
            scale_power = int_log_max;
        else if (int_log_max <= -maxCharsPerLabel)
            // we have an order of magnitude that can't be displayed in standard form, so we need to set
            // a value for scale_power
            scale_power = int_log_max;

        // System.out.println();
        // System.out.println("int_log_max = " + int_log_max + ", maxCharsPerLabel = " + maxCharsPerLabel + ", scale_power = " + scale_power);

        final DoubleNumberFormatter format = new DoubleNumberFormatter(scale_power);
        // the formatter uses scale_power when creating labels

       /*
        * Our strategy here is based on the observation that plotting the range
        * 0.2 to 40 is very similar to the task of plotting the ranges 2 to 400,
        * or 0.02 to 4.  We scale the min and max by an order of ten such that when
        * converted to integers (by a process not quite like trucation) the difference
        * between those two integers is a number greater than or equal to 10 and less
        * than (but not equal to) 100.  In other words, there will be a difference of
        * exactly two digits, which is a sensible precision to see in variation between
        * axis labels.  For example, the ranges listed above would all yield the integer
        * pair 1 to 40 (because the difference between those integers has two digits).
        * Given a pair of integers, the function proceeds to calculate appropriate labels.
        * If we are using the suggested range, we just round the min down to the next nice
        * label and we round the max up to the next nice label (unless the max and min are
        * already on nice labels).
        */

        final double difference = data_max - data_min;
        final double pow = Math.floor(Math.log(difference) / log10) - 1.0;
        // pow is the power of 10 used to get integers of the appropriate range

        int fractDigits = 0;
        if (scale_power > 0)
            fractDigits = scale_power - (int) pow;
        else if (pow < -0.5)
            // we use -0.5 instead of 0.0 in case a Math.floor() returns something that should be 0.0
            // but is really just barely under 0.0
            fractDigits = scale_power - (int) pow;

        final double conversion = Math.pow(10.0, pow);
        // this is the actual conversion factor we used, stored once to keep from
        // having to recalculate it

        int intMin = round(data_min / conversion, false);
        int intMax = round(data_max / conversion, true);
        // we now have intMin and intMax: the integer pair with a two-digit difference

        plot_min = data_min;
        plot_max = data_max;

        final int naturalNumberOfDivisions = intMax - intMin;
        // this number has precisely two digits

        int nDivisions = 0;

        final float idealMinFraction = 0.5f;
        // we will allow as few as this fraction of the maximum number of labels if it is convenient

        int nUnits = 1;
        // this number can represent two things:
        //  a) if naturalNumberOfDivisions < maxNumberOfDivisions
        //     it represents the number of divisions (units) between the natural divisions
        //  b) if naturalNumberOfDivisions > maxNumberOfDivisions
        //     it represents the number of units between divisions (the number to skip between divisions)
        // 1 is the default value, but we will see if a different value might be better

        if (naturalNumberOfDivisions < maxNumberOfDivisions)
        // we might like to put in some new divisions
        {
            final float proximity = (float) naturalNumberOfDivisions / (float) maxNumberOfDivisions;
            // this number measures how close the natural number is to the maximum

            boolean niceDivisionFound = false;
            if (proximity < idealMinFraction)
            // we want to do something because the number we have is below the range we want
            {
        	final int[] divisions = {2, 4, 5, 10, 20};
        	// These are the numbers of subdivisions we might want to place between natural divisions.
        	// The array only goes up to 20 because we would need a plot with at least 200 labels before
        	// needing to go any higher.
        	for (int i = 0; i < divisions.length; i++)
        	{
                    final int candidate = divisions[i];
                    if (proximity * candidate <= 1.0)
                    // this might work, because the number is small enough that we could fit
                    // this many divisions on the axis
                    {
                	niceDivisionFound = true;
                	nUnits = candidate;

                	// the next iteration might be even better, so we...
                	continue;
                    }

                    // if we didn't execute continue, it was because we can't fit this many
                    // divisions on the axis, and so there's no point in trying the next
                    // iteration either because it's even bigger, so we break out of the loop
                    break;
        	}
            }
            if (niceDivisionFound)
            {
                    nDivisions = naturalNumberOfDivisions * nUnits + (int) ((plot_max / conversion - intMax) * nUnits);
                    /*
                     * The first term in the expression isn't tough: If nUnits is 2 then we need twice as many
                     * divisions on the axis.  The second term isn't so obvious.  Suppose our axis goes from 0
                     * to 12.7 and we decide to set nUnits to 2.  We therefore get labels 0.0, 0.5, 1.0, ... , 12.0
                     * but we won't get 12.5 on there.  There will instead be empty space where the 12.5 should go.
                     * The last term accounts for this, by taking the difference between the top label and the
                     * axis max (in this case 12.7 - 12.0 = 0.7), multiplying bn nUnits (to get 1.4 in this case)
                     * and truncating (to get 1 in this case).  The result (1 in this case) is the number of labels
                     * extra we need.
                     */

        	if (pow < 0.5 || scale_power > 0)
        	// we use 0.5 instead of 1.0 in case a Math.floor() returns something
        	// that should be 1.0 but is really just barely under 1.0
                    fractDigits++;
                    // we've gone down to one lover decimal level so we have to tell the formatter

        	if ((nUnits == 4 || nUnits == 20) && (pow < 1.5 || scale_power > 0)) // we've actually gone down two decimal levels, so...
                    // we use 1.5 instead of 2.0 in case a Math.floor() returns something
                    // that should be 2.0 but is really just barely under 2.0
                    fractDigits++; // we add another
            }
            else
            // we give up and keep the natural number, even though it's smaller that what we'd like
            {
        	nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);
            }
        }
        else if (naturalNumberOfDivisions > maxNumberOfDivisions)
        // the natural number is larger than what we'd like, so we have to skip over some
        // (typically this is the more common problem)
        {
            nDivisions = 1;
            // we supply this initialization to make the compiler happy, but in the algorithm
            // requires that this initial value change

            final int[] skips = {2, 5, 10, 20, 25, 50};
            // These are the numbers of natural divisions we're going to try skipping.

            for (int i = 0; i < skips.length; i++)
            {
        	final int nDivisionsThisTry = naturalNumberOfDivisions / skips[i];
        	if (nDivisionsThisTry > maxNumberOfDivisions)
        	{
                    // this many skips isn't big enough, so we'll try the next one
                    continue;
        	}

        	// we now assign to nUnits the number of natural divisions to skip over
        	nUnits = skips[i];
        	nDivisions = nDivisionsThisTry;

        	if (nUnits >= 10 && nUnits != 25 && fractDigits > 0)
                    // we're skipping at least an order of 10, and we're not in quarters, so...
                    fractDigits--; // we can get rid of one fraction digit

        	/*
        	 * We may calculate a new value for the intMin.  Consider, for example,
        	 * the range 3 to 17.  If we decide that our skip will be 2, we will get
        	 * labels like 3, 5, 7, 9, etc.  This will look dumb because we would much rather
        	 * have the first label a nice multiple of our skip (i.e., we would rather
        	 * have 2, 4, 6, 8, etc., or for multiples of 5 we would rather have 20,
        	 * 25, 30, 35, etc. over 18, 23, 28, 33, etc.)  Therefore, if the intMin is not
        	 * a nice multiple of that skip then we increase the intMin, and we may have
        	 * to decrement nDivisions because of a lost label.
        	 */
        	if (intMin % nUnits != 0)
        	// only true if we are not using the suggested range
        	{
                    // increase is the amount we will increase intMin by to make it a nice multiple of nUnits
                    final int increase = intMin > 0 ? nUnits - intMin % nUnits : -intMin % nUnits;

                    if (increase > intMax - (intMax - intMin) / nUnits * nUnits - intMin)
                	// we have put the last label over the limit, so...
                	nDivisions--; // we drop the last label

                    intMin += increase;
        	}

        	// We are happy with what we've got because it gives us an acceptable
        	// number of divisions.  We don't want to go any higher because that
        	// will just make for fewer divisions, so we...
        	break;
 	    }
        }
        else // hey! they're exactly equal
            nDivisions = Math.max(naturalNumberOfDivisions, minNumberOfDivisions);

        double minLabelValue = intMin * conversion;
        final double inc = naturalNumberOfDivisions < maxNumberOfDivisions ? conversion / nUnits : conversion * nUnits;
        if (naturalNumberOfDivisions < maxNumberOfDivisions && minLabelValue - inc >= plot_min)
        // this happens if we are dividing up divisions, and we get divisions below intMin * conversion
        {
            int nLost = (int) ((minLabelValue - inc) / inc);
            minLabelValue -= nLost * inc;
            nDivisions += nLost;
        }
        labels = new AxisLabel[nDivisions + 1];
        this.nDivisions = nDivisions;

        // System.out.println("fractDigits = " + fractDigits + ", minLabelValue = " + minLabelValue + ", inc = " + inc);

        format.setFractionDigits(fractDigits);
        for (int j = 0; j < labels.length; j++)
        {
            final double labelValue = minLabelValue + j * inc;

            labels[j] = new AxisLabel();
            labels[j].text = format.format(labelValue);
            labels[j].position = (labelValue - plot_min) / (plot_max - plot_min);
            // labels[j].logPosition = Math.log(labels[j].position + Math.E);
            if (labels[j].position < 0.) labels[j].position = 0.;
        }
    }

    private int charsReq(int pow)
    /*
     * Returns the number of characters required (not using scientific
     * notation) to represent a decade of the given order of magnitude.
     */
    {
        if (pow < 0)
            /*
             * If the power is less than 0, we need one space for the
             * leading zero, one for the decimal, and then -pow spaces
             * for each of the following characters.  For example, if
             * pow = -2 then the result would be 4, because there are
             * four characters in the string "0.01".
             */
            return -pow + 2;
        else
            /*
             * If the power is 0 or greater, we need one character for
             * the character '1' and one '0' for each order of magnitude.
             */
            return  pow + 1;
    }

    private int round(final double d, final boolean down)
    /*
     * Determines an integer value from a double by rounding intelligently.
     * In the logarithmic case, when determining the order of magnitude
     * of the lowest tick, the "down" parameter is false because
     * we want to round up from the minimum point in the axis (so that
     * the tick shows up on the range of the axis) if we are not very close
     * to an integer value.  However, when we are determining the order
     * of magnitude of the highest tick, we want to round down if we are
     * not very close to an integer so that the tick appears within the
     * range of the axis.  Similarly in the case for the linear axis, we round
     * up to get the smallest tick value and we round down to get the largest
     * tick value.  We do exactly the opposite if wew are using the suggested
     * range.  In that case, we round down to get the minimum and up to get
     * the maximum.
     */
    {
        final double minProximity = 0.0001;
        /*
         * A parameter's proximity to the nearest integer must be this
         * fraction of its size in order to be considered that value.
         */
        final double round = Math.round(d); // the closest integer value
        if (d == round || Math.abs(d - round) < (d != 0.0 ? minProximity * Math.abs(d) : 0.000001))
        {
            // we assume here that d is close enough to be an integer, so we round and return
            return (int) round;
        }
        else
        {
            // d is not close enough to be an integer, so we return the
            // floor if we were supposed to round down and ceil
            // otherwise
            return down ? (int) Math.floor(d) : (int) Math.ceil(d);
        }
    }

	public String [] getLabels()
	{
		int len = labels.length;
		String [] lab = new String [len];
		for (int i = 0; i < len; ++i)
			lab[i] = labels[i].text;
		return lab;
	}

	public double [] getPositions()
	{
		int len = labels.length;
		double [] pos = new double [len];
		for (int i = 0; i < len; ++i)
			pos[i] = labels[i].position;
		return pos;
	}

    public void printLabels()
    {
 	 System.out.println("data_min = " + data_min + ", data_max = " + data_max);
	 System.out.println("plot_min = " + plot_min + ", plot_max = " + plot_max);
	 System.out.println("nDivisions = " + nDivisions + ", labelsValid = " + labelsValid);

	 int i;

	 for (i = 0; i < labels.length; ++i)
	 {
	 	System.out.println("label "+ (i+1) + ": " + labels[i].text + "  position: " + labels[i].position);
	 }
    }



   // This class is used when storing information about
   // the labels on the axis.
   private class AxisLabel
   {
	   String text;
	   double position;
           // double logPosition;
   }

   /**
    * Just for testing
    */
   public static void main(String[] argv)
   {
       AxisLabelCalculator t = new AxisLabelCalculator();
       t.createNewLabels(0., 1.);
       t.printLabels();
       t.createNewLabels(0., 100.);
       t.printLabels();
       t.createNewLabels(0.0001, 0.001);
       t.printLabels();
       t.createNewLabels(5., 10.);
       t.printLabels();
       t.createNewLabels(.001, .050);
       t.printLabels();
       t.createNewLabels(.00001, .00005);
       t.printLabels();
       t.createNewLabels(-.001, .00005);
       t.printLabels();
       t.createNewLabels(-.001, .005);
       t.printLabels();
       t.createNewLabels(-2., 5.);
       t.printLabels();
       t.createNewLabels(.05, 10.);
       t.printLabels();

    }

}
